文章目录
  1. 1. View与ViewGroup类结构
  2. 2. View与Window
  3. 3. View绘制与重绘过程
  4. 4. View与ViewGroup事件分发
    1. 4.1. 事件产生
    2. 4.2. 事件分发和处理
    3. 4.3. 事件冲突

本文基于Android 4.0 源码

View与ViewGroup类结构

View与ViewGroup的类关系

View与Window

Android系统中所有的UI类都是建立在ViewViewGroup基础上。View的子类包括widget包中的那些(常用的TextViewImageView等都在这个包下),ViewGroup的子类包括Layout命名的类以及RecyclerView等少数以View命名的类,ViewGroup又是View的直接子类,这样就允许ViewViewGroup并列与嵌套存在。在AS中可以通过F4查看类的继承关系。

Window不继承于View,它就是一个用来显示东西的窗口,通过setContentView来显示东西(调用该方法之后紧接着调用initWindowDecorActionBar来设置ActionBar),Activity自身持有一个mWindow(初始化为PhoneWindowPhoneWindow的定义在源码目录frameworks/policies/base/phone/com/android/internal/policy/impl/PhoneWindow,它是Window的直接子类,被标注为@hide,SDK看不到,需要下载SDK源码才能看到,在SDK源码的android-23/com/android/internal/policy/PhoneWindow下),ActivitysetContentView就是mWindow.setContentView的代理。PhoneWindow有个DecorView内部类(继承自FrameLayout),在初始化PhoneWindow的时候会创建一个DecorView的实例作为根视图。

还有一个类叫做LayoutInflater,它本身是个抽象类,但只有一个cloneInContext是抽象方法。它采用工厂模式负责将布局文件xml实例化为View对象。看它的inflate方法,最终调用的都是View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)这个方法,用XmlResourceParser从resource中获取parser,然后同步地完成这些调用:
result=root, attrs=Xml.asAttributeSet(parser)
-> temp=createViewFromTag(root, name=parser.next().getName(), inflaterContext=mContext, attrs)
-> root!=null?(params=root.generateLayoutParams(attrs);if(!attachToRoot)temp.setLayoutParams(params)):null
-> rInflateChildren(parser, temp, attrs, true)
-> if(root!=null && attachToRoot) root.addView(temp, params); if(root==null || !attachToRoot)result = temp
-> return result
大致意思是首先解析xml,找到第一个开始的Tag即布局文件的根视图的标签,把它和布局文件的AttributeSet传给createViewFromTag就获得了布局文件的根View,如果root非空且要附加到root上就把这个View附加上去。在处理Tag时单独处理MERGE和INCLUDE两个Tag。在返回前调用了rInflateChildren方法来填充这个View的子视图,这是个递归填充子视图的方法,根据Tag名分别调用parseRequestFocus parseViewTag parseInclude createViewFromTag&&rInflateChildren来一层层创建View,在finishInflate的时候View提供了onFinishInflate回调供子类重写。

这里就有三个东西了,Window、Inflater、View,他们之间的关系就好像画布、画笔和成品作品,xml布局文件就相当于设计图,Activity作为画家需要将成品按照设计图一层层画到画布上。
给个图来表示:
ActivityWindow

View绘制与重绘过程

inflate只是相当于给画作打了个线稿,规定了要画的内容的层次关系,具体画什么、在哪里画,就是View的绘制过程。Android中为View和ViewGroup设计了一致的绘制流程。绘制视图总要有个开端,仍以Activity为例,前面说到Activity通过setContentView用Inflater把xml导入成View树,这时候我们只是在逻辑上得到了View的结构,还没有实际画到屏幕上。猜想Activity载入了View之后,应该是在某个生命周期的时候绘图,回想Activity总是在onStart之后才能看到Activity内容,跟随Activity的生命周期,猜想是某个Manager比如ActivityManager在控制。随便找了一下Activity的startActivity方法,发现Activity中所有的startActivity实际上都是调用的startActivityForResult方法(它有两个重载),它里面又是调用的Instrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this,intent, requestCode, options),在这个方法中使用ActivityManagerNativegetDefault().startActivity()真正开始Activity,查看源码知道getDefault()得到的是ServiceManager.getService("activity"),是系统提供的名为activity的服务,它经过asInterface转换成IActivityManager接口后作为结果返回,显然它的实现在Server端,同时有内部类ActivityManagerProxy用来调用远程服务,前面分析AIDL和Binder机制时候说过,实际上对远程服务的调用中,系统Binder驱动和Server各持有一个服务的实例,其中系统持有的是代理。通过queryLocalInterface区分是从服务端还是客户端进行服务调用,从服务端调用就直接返回本地服务了,从客户端调用则通过代理来访问服务。

这里暂时停下,看到有一个ActivityManager类,很有意思,它的内部类AppTask有个startActivity方法,首先获取ActivityThread,再通过Instrumentation.execStartActivityFromAppTaskActivityThreadApplicationThread上开始Activity,点进去一看它实际上是调用的参数中的IAppTask.startActivity,回来看传入的参数是mAppTaskImpl,它的值由构造函数确定,而AppTask的构造函数是隐藏的,这很Android。顺藤摸瓜发现它在ActivityManager的getAppTasks方法中调用,实际上是从ActivityManagerNative.getDefault().getAppTasks(mContext.getPackageName())得到的任务列表,好嘛,又看到ActivityManagerNative了,可问题在于,这货的AppTask又是从哪儿得到的呢?

以上花了不少力气,结果只看明白了Activity的启动是系统服务负责的,虽然通过查看SDK源码的ServiceManager找到了启动Activity的系统服务,但是仍然没有看到控制Activity绘制内容的相关代码,解耦解得厉害就是有这点不好,全都是面向接口了不太好找实现。用了一个土方法,根据分包直接去找activity服务的实现代码,感谢Android良好的模块设计,很快就找到了com/android/server/am/ActivityManagerService.java,一看它继承了ActivityManagerNative,稳了。直奔startActivity方法发现原来用的是startActivityAsUser方法,它又调用的ActivityStackSupervisor.startActivityMayWait,这个方法挺长的,关键的一步是调用startActivityLocked,里面调用doPendingActivityLaunchesLocked,它又调用startActivityUncheckedLocked,(一路各种设置flag,最后调用了ActivityStackstartActivityLocked,在Activity当前栈栈顶添加一个ActivityRecord,再调用WindowManager.addAppToken在task中存下Token,并且记录<Binder,Token>对应关系,有空单独写个Activity的启动,这里就不再说了),惭愧看很久也没看到开始Activity的具体代码。求助Google,然后得知ActivityThread(同样是@hide,在SDK中看不到)是启动Activity的关键类,它运行performLaunchActivityhandleResumeActivity来创建和启动Activity。看到performLaunchActivity的第一个参数是ActivityClientRecord类型,感觉自己前面的功夫没有白费,在这里连接起来了。这里通过Instrumentation(android/app/Instrumentation)依次创建了ActivityApplicationContext,然后又依次调用了Activity的OnCreateperformStartOnRestoreInstanceStateOnPostCreate。现在还没有调用OnResume,理论上应该还没有绘制内容。performStart方法也只是使用Instrumentation调用了Activity自身的OnStart,并且将Start事件分发到mFragments而已。

来看handleResumeActivity,上来先是一记performResumeActivity(一番状态设置之后调用activity.performResume),一堆状态读取和设置后,if(r.activity.mVisibleFromClient)r.activity.makeVisible(),这个方法看起来很有希望就是我要找的方法!找去源码一看,方法内容出奇的简单:mDecor.setVisibility(View.VISIBLE)可以说就这么一句。靠!根本还没完!


接着看,原来ViewsetVisibility所调用的setFlags方法会在传入参数为VISIBLE时调用invalidate(true)方法(顺便一提setFlags方法当然也会处理GONEINVISIBLE,再顺便一提这就解释了为什么设置可见性后不需要调用invalidate就可以自动重绘)。最终还是走到了View源码。

打开invalidate(boolean invalidateCache),调用的invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate),这里出现了一个神奇的mGhostView,之后还记得的话再来看看它是干嘛用的。l t r b是需要invalidate的区域,invalidate方法传入的直接就是View实例的整个区域。调用mParent.invalidateChild来invalidate给定区域。mParent只是一个ViewParent接口,它具体是指向什么实例还是要看代码。想一想Activity中什么内容都不设置的话,是有一个DecorView和一个id为content的FrameLayout,应该可以在FrameLayout或者DecorView找到具体的实现代码。幸运的是,在FrameLayout的父类ViewGroup中找到了invalidateChild方法(ViewGroup实现了ViewParent接口)。

方法挺长,拣重点说。parent变量初始化为ViewGroup自身,先标记出dirty区域(为transformMatrix变形后的dirty区域上下左右各扩大0.5f),然后调用parent的invalidateChildInParent方法对dirty区域进行扩展并将返回值赋予parent,如此往复直到parent为null为止。ViewGroup的这个方法返回mParent成员变量,它是一个隐藏的变量,从它的名字和可能为null来推断应该是会随着这个方法的调用而变化(我猜它会层层向上直到根视图DecorView的父ViewRootImpl,这个东西之前没有提过,其实它就是最顶层DecorView所添加到的地方, DecorView实例作为一个成员变量mView存在于它的实例中, 它受到WindowManager的实现类WindowManagerImpl实际上是WindowManagerGlobal的管理,它是逻辑上存在的一个东西,类型也不是View,但实现了ViewParent接口),且是受系统什么服务控制的,我猜是WindowManager。在这个过程中如果如我所猜是层层向上,那么总会到达DecorView,而DecorView的父就是并不属于View子类的ViewRootImpl,当此时就会调用到ViewRootImplinvalidateChildInParent方法,有意思的是这个方法最终返回null,这从侧面验证了我的猜测可能是对的。

这里有一个关键变量,即mAttachInfo.mInvalidateChildLocation,这是一个仅两个元素的整型数组,分别存储child的left和top,从名字看存的是要invalidate的child的位置,真正绘制应该跟这个数组有关。

至此我发现前面整个过程是一个计算dirty区域的过程,想想图形的出现和更新其实都可以看做是对特定区域的绘制,通过计算dirty区域就统一了这两个行为。到了ViewRootImpl总算是计算完了dirty区域,接下来总应该画图了吧。一看果然,在ViewRootImplinvalidateChildInParent方法中调用了invalidateRectOnScreen,而这个方法里又有个scheduleTraversals方法的调用,看起来就像是要遍历重绘的意思,它的代码是往Looper队列里添加了一个同步屏障,并在mChoreographer里添加了一个TraversalRunnable的回调,跑去这个Runnable的定义一看,就是调用了doTraversal方法,方法内容是移除同步屏障并执行performTraversals方法。这样想来,估计窗口第一次显示的时候类似的遍历过程会在WindowManagerGlobal调用addView方法之后调用吧。

先不去管他,咱们来看performTraversals方法。……………………好长,里面写了一些debug的print,有空的话可以把debug开关打开来profile一下。这里挑重点看:首先是判断了一番当前的状态,是否需要创建Surface,是否需要完全重绘,是否需要硬件渲染,根据这些状态设置相应的条件,在满足Measure的条件时,调用performMeasure进行测量(该方法的内容是调用mView.measure(),在View的该方法实现代码中,调用了onMeasure,而DecorViewonMeasure方法在计算好自身宽高以后,调用了父类的onMeasure方法,我们还记得DecorView的父类是FrameLayout,去查看它的onMeasure方法就会发现,它对每一个child调用了measureChildWithMargins进行递归measure,然后才调用setMeasuredDimension设置自己的宽高),measure不好还有可能measure第二次,测量完了之后layoutRequested=true; 接下来就执行了performLayout进行布局,方法中首先调用host(顶层mView的缓存)的layout进行布局(仍然调用的父类的FrameLayout.onLayout方法,非常暴力,就是一个layoutChildren进行递归layout),然后计算有效的布局请求的View数量,如果此时还有需要布局的View则执行以下三步:遍历调用View.requestLayout,接着调用measureHierarchy,然后是host.layout再走一遍。这三步是第二次layout了,此时再检查还有没有需要布局的View,如果还有就只好将遍历调用View.layout的任务post到队列中等待下一帧执行,该方法完成后源码中注释说此时所有View就已经计算好并放置好了,接下来就会计算透明区域,主要就是把透明区域搜集起来并设置,如果透明区域跟之前的不同,则要求完全重绘;此时已经知道了屏幕上哪些地方需要重绘,哪些地方是透明的,接下来判断是否cancelDraw,mAttachInfo.mTreeObserver.dispatchOnPreDraw()完成(AttachInfoView的内部类,当某个View附着到它的父窗口时,这个类的实例就存储一些相关的信息)或者viewVisibility!=VISIBLE都会被记为cancelDraw,如果可见性为VISIBLE则此时还会再试一次scheduleTraversals。只有在经过前面的状态判断后仍然需要绘制的情况下,才会调用performDraw()进行绘制,该方法调用draw()方法,在draw()中首先mAttachInfo.mTreeObserver.dispatchOnDraw()将事件传给观察者(dispatchOnDraw方法中,依次调用已注册的OnDrawListener中的onDraw()方法),然后根据情况调用mAttachInfo.mHardwareRenderer.draw或者drawSoftware(在drawSoftware中会调用mView.draw,查看DecorView源码知道其实主要是调用的super.draw,再看FrameLayout源码发现未被重写,再往上到ViewGroup的源码,很棒仍然没找到draw不过找到了drawChild,再往上看,就到View了,Viewdraw方法中会先调用drawBackground,再调用onDraw来画自身内容,然后调用dispatchDraw请求孩子进行绘制,最后调用onDrawForeground。然而ViewdispatchDraw是个空方法,根据逻辑它应该是个ViewGroup来实现,于是回来找到ViewGroup,果然里面有dispatchDraw的实现代码,这个方法大体还是对每个child调用drawChilddrawChild方法只是返回child.draw的结果,至此完成递归draw),这期间有可能再来一次scheduleTraversals

看到这里有个疑问,dirty区域哪去了?其实回头一看就会知道,dirty早在invalidateRectOnScreen的时候就被合并到mDirty中了,而mDirty会在draw方法中使用。想必各位也注意到了,只有draw是要先dispatchOnDraw的,这暗示了这三个重要的步骤的逻辑是前两者由孩子发起,到父亲结束,而绘制动作是由父亲发起。但三个步骤的实际执行都是先执行孩子的对应方法,才执行父亲的对应方法。回想一下,一切的开端都在View调用invalidate(true)的时候。至此,整个重绘的过程就已经清楚了。

那么,当一切开始时,Activity是怎样从零开始绘制DecorView的呢?借助前面重绘过程的探索分析,我发现了在执行ActivityThread#handleResumeActivity时会调用r.activity.makeVisible()方法使activity可见,这个过程就是上面我们分析过的测量和绘制视图的过程了。那么这些视图是什么时候初始化的呢?从setContentView到视图可见,之间经历了什么?其实在makeVisible()这个调用之前,在performResumeActivity之后,还有一个重要的步骤,就是添加DecorView,具体是先通过r.window = r.activity.getWindow()获取Activity所在的Window,并记录到r中,一般来说这个Window就是PhoneWindow了,所以接下来就不难理解调用r.window.getDecorView()获取到了DecorView的引用。获取到DecorView引用后,首先设置为INVISIBLE,然后通过ViewManager wm = a.getWindowManager()获取到ViewManager实例,通过WindowManager.LayoutParams l = r.window.getAttributes()获取到布局参数,最后通过wm.addView(decor, l)添加DecorViewWindowManager中。

来看addView,前面讲过这里的WindowManager实际上是WindowManagerGlobal,所以我们知道这里实际调用的是WindowManagerGlobal.addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow)(这里的后两个参数来自于WindowManagerImplmDisplaymParentWindow),它做了什么呢?打开源码,看到它首先使用params参数调整父窗口的子窗口(parentWindow.adjustLayoutParamsForSubWindow(wparams)),然后同步地先确保之前的removeView()执行完毕,再用View.getContext()实例化了ViewRootImpl并赋值给局部变量root,同时分别在mViews mRoots mParams添加记录view root params。同步块之后调用了root.setView(view, params, panelParentView)设置视图内容。这个setView方法厉害了,它整个都是同步的,跳过一些我暂时看不懂的SurfaceHolder等内容后,首先设置mAttachInfo.mRootView = view,然后调用了requestLayout,然后调用mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel)mWindow加入到WindowManagerGlobal中,然后view.assignParent(this)ViewRootImpl注册为DecorView的父亲。其中mWindowSessionWindowManagerService的远程事务。至此,DecorView就已经被加入到ViewRootImpl并随着PhoneWindow加入到了WindowManagerService中接受其管理。

总结一下,可以看到View的绘制过程和重绘过程其实本质上是一样的,只不过绘制的dirty区域是整个窗口。在Activity初始化的时候,在ResumeActivity之后,真正显示之前,ActivityThread进行了Activity中DecorView的创建、初始化和添加,然后通过调用setVisibility来间接调用invalidate方法进行重绘,在重绘时子View通过调用invalidate方法,请求父亲计算dirty区域并递归依次按需执行measure、layout、draw三种方法。孩子节点要进行重绘的时候,只需要在设置好新的界面值之后直接或间接调用invalidate方法,就可以触发该孩子节点及其所有祖先的重绘。源码为了健壮性,有很多地方都可能进行两次甚至三次某个步骤,有些地方甚至直接重新全部执行,过多次数的计算和重绘可能会产生性能瓶颈。

View与ViewGroup事件分发

在前面我们已经见到了两个神奇的方法dispatchOnPreDrawdispatchOnDraw,这一节我们来看更多的dispatchXXX, 也就是Android中的事件分发机制。多一句嘴, View的事件分发机制只是针对触屏事件的一种处理思路, 这种思路完全可以用在别的事件的分发和处理流程上.

我们知道, 对于用户来说, 跟Activity交互的主要方式之一就是触屏, 而展示在用户面前, 用户可以看见的东西, 就是我们的 ViewViewGroup, 所以 ViewViewGroup 必然会对触屏事件做处理. 这里我们直接通过读源码的方式去了解View的时间分发机制, 并着重根据以下几种触屏事件来梳理思路, 加深理解:

  1. 点击事件(Click)分发
  2. 按下事件/释放事件
  3. 滑动事件

同时, 我们需要注意一个事件的处理周期, 即它分发的路径是怎样的, 是否会停止分发, 满足什么条件会停止分发.
对事件分发机制有充分理解, 那么各种按键事件响应处理和事件冲突问题, 就可以迎刃而解.
写这一节参考了网上的一些资料, 这里先做个说明, 本节不会以”消费”来表示事件处理函数返回true的情况, 纯粹因为我个人不喜欢这个词, 觉得表意不明确, 我认为方法的返回值只是表示”是否停止处理事件”, 而这个意思用”消费”来表示不太准确, 尽管源代码中用来表示这种情况的Consume通常确实被翻译为消费. 各位读者如果习惯这样称呼, 也并无不妥. 其他内容如有引用会在文中注明.
本节行文方式是先背景知识, 再总体流程图, 再贴源码详细解释.

事件产生

触屏对操作系统提供硬件接口用以通知操作系统产生了什么事件, 操作系统通过硬件驱动读取和解析硬件接口的事件, 这个跟源码关系不大我们就不说了. 我们只说当事件被Android系统捕捉到之后, 传入Activity的时候是个什么形式, 没错, 就是MotionEvent. 硬件输入事件被InputEvent来描述, 该类是一个抽象类, 其有两个直接子类, 即MotionEventKeyEvent, 分别对应触屏事件和按键事件. 我们这里重点关注MotionEvent.
它在frameworks/base/core/java/android/view/MotionEvent.java中定义, 比较大, 注释也多, 我们只抽象地简单了解一下它刻画了触屏事件的哪些属性.
首先它定义了一系列ACTION常量来表示触屏事件, 例如我们下面会用到的ACTION_DOWN ACTION_MOVE ACTION_UP, 以及ACTION_OUTSIDE ACTION_CANCEL ACTION_POINTER_DOWN ACTION_POINTER_UP ACTION_SCROLL等.
其次它还定义了触屏事件的坐标属性包括偏移量等信息, 对应的处理和转换方法(@hide), 以及相应的Getter.
最后它定义了触屏事件的发生时间, 以及一系列toString方法.
这里面有很多方法都是native的, 而且也用到了aidl可见跟binder驱动估计也有关系. 如果今后有时间, 我会深入去看一下硬件到事件的过程, 今天先点到为止.
MotionEvent本身感兴趣的同学可以去看看源码和官网的文档, 也可以在网上搜一下, 已经有很多人写过相关的博客.

事件分发和处理

事件产生之后(MotionEvent), 是怎么分发和处理呢? 我们先来看一张图:
事件分发总流程
上图来自图解 Android 事件分发机制. 图中白色箭头代表事件流动的方向. 大体上来说一个全新的事件就是按照这个过程分发和分级处理的.
可见事件处理的主体主要分三个层级, 即Activity ViewGroup View, 你没看错, Activity这个浓眉大眼的家伙也可以处理MotionEvent.
网上常说”从上往下传递事件, 从下往上处理事件”, 其实默认是从”View树层次结构”的角度去说的, 而不是”View的显示层次”, 在”View树层次结构”这个角度上, Activity及其DecorView就是最上层, 逐层往下是ViewGroupView, 这其实是一个深度优先搜索的过程, 对于一个确定的MotionEvent, 在这棵树上的路径也是确定的. 其实每一层都是先往下搜索看子节点是否处理完毕这个事件, 如果处理完毕了, dispatchTouchEvent就返回true, 否则本层调用onTouchEvent进行处理并返回处理结果.
所以我们可以猜测代码逻辑是这样:

public boolean dispatchTouchEvent(MotionEvent e){
    View target;
    for (View child: children){
        target = child.getTouchTarget(e); // find event target
        if (target != null){
            break;
        }
    }
    if (target != null && target.dispatchTouchEvent(e)){
        return true;
    }

    return onTouchEvent(e);
}

当然事实上远没有这么简单, 在ViewViewGroup中, 在实际分发和处理事件之前, 进行了一系列的逻辑处理(这里我就不贴源码了, 有兴趣的同学可以自行查看), 如: 在ViewGroup中, 判断当前是DOWN事件或者目标非空, 如果满足条件, 则设置一系列标记, 并判断该事件是否被自己拦截(onInterceptTouchEvent). 如果不满足条件, 即非DOWN事件且目标为空, 则直接标记该事件被拦截, 随后关键步骤是调用dispatchTransformedTouchEvent, 根据传入参数来决定是调用孩子节点的onTouchEvent还是调用super.dispatchTouchEvent(即View.dispatchTouchEvent, 这条路径会让自己的onTouchListeneronTouchEvent处理事件); 在View中, DOWN事件会让View停止滚动, 判断是否被OnTouchListener处理完毕(该步取代了dispatchTouchEvent, 原因显而易见), 如果没有则调用OnTouchEvent处理; 以上两者都会判断事件是否满足一致性校验, 是否满足安全性策略, 是否被取消.
与此相比, Activity中的事件处理就是一股清流, 它的代码非常简短, 几乎就跟我上面给出的差不多. 它先将DOWN事件交给onUserInteraction处理, 然后并不返回, 继续将该事件交给getWindow().superDispatchTouchEvent, 如果未被处理完毕则交给自身的onTouchEvent处理并返回.
神秘的就在于superDispatchTouchEvent了, 复习一下第一节的知识知道它八成是在PhoneWindow中定义的. 打开一看, 果不其然, 又被mDecor代理了…我们回忆一下, DecorView这个内部类是继承自FrameLayout的, 也就是说他是个ViewGroup, 好, 我们来看看这个类中superDispatchTouchEvent怎么定义的:

public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
}

直接丢给父类了, 人干事? 好在我们惊喜地发现这个方法是public的, 我们可以自己定义Window.superDispatchTouchEvent. 当然, 最惊喜的是getWindow()也是public的, 使得自定义Window并修改事件分发策略成为可能, 这个设计绝赞.

现在我们已经大体知道了事件分发和处理的逻辑, 不清楚读者心中产生了什么新的疑问, 我有两个疑问: 1. 事件从何处传递给Activity(我们怎么确定顶层处理组件是Activity而不是其他什么东西?); 2. 事件何时终止传递, 连续事件(DOWN->MOVE->UP)有时候是要同一个View进行处理的, 例如滑动ListView, 如何保证这个过程?

对于第一个问题, 我自己想想就觉得头大, 首先它肯定是硬件传递给我们操作系统再分发给某个服务(ActivityManagerService?)的, 这里面估计涉及到Binder通讯, 分析Binder原理的时候要了我半条命(感兴趣的同学可以在我博客里搜索到Binder原理解析的文章). 其次, 对于裸的驱动事件, 必然需要借助设备驱动将事件进行重新包装和定义, 这一步可能是Android提供接口, 设备驱动来完成, 也可能是Android系统自己来完成, 具体实现过程不太好找. 没办法, 有此疑问必然辗转反侧, 所以还是来看看吧. Window.Callback接口类定义了dispatchTouchEvent这个接口, 但是对于Activity来说哪个调用了它的这个方法, 我一点线索也没有. 回忆应用启动过程, 应该可以从MotionEvent的构造方法找到一点蛛丝马迹. 具体艰辛的过程我就不细述了, 总之最后终于发现了如下流程:
Linux系统已经实现了对触屏事件的封装和传递, 硬件事件将会通过内核底层的硬件驱动进行处理, Android系统在此基础上进一步封装内核传递过来的触屏事件, 并根据事件的具体值, 依据硬件设备协议, 来初始化触屏事件MotionEvent.(回忆adb sendevent其实传输的就是触屏事件或按键事件)

然后事件被包装成QueuedInputEvent, 并且由被设置了CallbackMessage携带, 进入了主线程ActivityThread的消息队列, 处理时直接handleCallback调用了Callback, 而这个Callback是调用ViewRootImpl.dispatchInputEvent(接口来自InputEventReceiver)分发给了ViewRootImplViewRootImpl.onInputEvent处理, 它才是真正意义上第一个处理触屏事件的类, 在该类中经过一系列处理后会传递到以下代码中:

private int processPointerEvent(QueuedInputEvent q) {
    ...
    final MotionEvent event = (MotionEvent)q.mEvent;
    final View eventTarget =
            (event.isFromSource(InputDevice.SOURCE_MOUSE) && mCapturingView != null) ?
                    mCapturingView : mView;
    mAttachInfo.mHandlingPointerEvent = true;
    boolean handled = eventTarget.dispatchPointerEvent(event);
    ...
}

至于mView其实就是DecorView的实例, 是整个View树中最上层的可见元素, 见上文. 于是在这里调用了要么是mCapturingView要么是mViewdispatchPointerEvent, 而在View.dispatchPointerEvent的实现是, 如果它是MotionEvent, 则调用dispatchTouchEvent.
DecorView.dispatchTouchEvent中, 如果有Callback调用Callback.dispatchTouchEvent, 否则调用super.dispatchTouchEvent. 这就厉害了, 这里对于Activity来说, 它是实现了Callback接口的, 也就是说这里如果传入的CallbackActivity实例, 那么就是直接调用Activity.dispatchTouchEvent了, 一切都解释得通了.
来梳理一下这一段的调用:

ActivityThread.mH.handleMessage(Message msg) ->  
ActivityThread.mH.handleCallback(msg.callback)  
ViewRootImpl.dispatchInputEvent(q) ->  
ViewRootImpl.onInputEvent(mEvent) ->  
ViewRootImpl.processPointerEvent(q) ->  
DecorView.dispatchPointerEvent(q.mEvent) ->  
DecorView.dispatchTouchEvent ->  
Activity.dispatchTouchEvent ->  
Activity.getWindow().superDispatchTouchEvent ->  
DecorView.superDispatchTouchEvent ->  
FrameLayout.dispatchTouchEvent  

可以看到最后好像是绕了个圈, 从DecorView.dispatchTouchEvent出发, 又回到DecorView.super.dispatchTouchEvent.

对触屏事件的定义见代码./bionic/libc/kernel/common/linux/input.h.
触屏驱动见驱动代码目录./kernel/goldfish/drivers/input/touchscreen/.
事件封装见./frameworks/base/services/input/EventHub.cpp.

(如果有兴趣, 可以沿这条路径查阅代码, 感受一下我的艰辛: MotionEvent.obtain() -> ./frameworks/base/services/input/EventHub.cpp -> 实在找不到触发点, 写了个Demo打印调用栈, 发现是异步调用, 事件被放进ActivityThreadlooper中 -> 我能怎么办, 我也很绝望啊, 事件从哪里放进looper中的? 沿着处理栈看看吧 -> 惊喜发现)

至此, 第一个疑问基本得到解答. 第二个疑问是, 事件何时终止传递, 对于连续事件如何处理? 事实上, 从以上代码可以看到, 当任意一层dispatchTouchEvent提前返回而无论自身还是父亲节点都不再调用下一层dispatchTouchEvent的时候, 就终止事件传递了, 具体来说就是dispatchTouchEvent在调用孩子节点的dispatchTouchEvent之前就返回了true(例如, 被拦截或者传递给onTouchEvent并返回true). 连续事件分两类, 一类是像ACTION_MOVE这样的, 由一系列连续的事件点构成的一个事件, 另一类就是像我上文提到的那种, 按下不放滑动手指.

对于第一类, Android系统实际上将这种多次事件封装在了一个MotionEvent中, 并且用ACTION_MOVE这种标记来表示, 实际上我们手指触摸屏幕的时候, 很难达到仅有ACTION_DOWN->ACTION_UP, 中间多少都会夹几个ACTION_MOVE.

对于第二类, 其实也很简单, 在DOWN事件发生时, “ACTION_DOWN在哪个View的onTouchEvent返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程… 如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。“(以上引用自图解 Android 事件分发机制).
现象好像确实如此, 但是代码是怎么做到的呢? 带着好奇我重新看了ViewGroupView的事件分发, 发现了实现方法. 对于View, 如果在dispatchTouchEvent时调用了默认的onTouchEvent, 并且它是可点击的, 那么DOWN UP MOVE CANCEL四个事件会进入该函数的switch, switch之后必然返回true, 即触屏事件被处理完毕. 由于View本来就是事件分发的最底层, 所以它并没有显示后续事件拦截是如何实现的. 对于ViewGroup, 由于它并没有重写onTouchEvent方法, 故所有事件如果传递到了onTouchEvent则必然也是走View的方法. 在dispatchTouchEvent过程中, 特殊处理DOWN事件, 此时会清除掉内部保存的target链表, 并遍历一个有序的View[] children数组(按绘制先后顺序排序), 对于每一个孩子节点, 在target链表中寻找是否存在它对应的target, 如果存在则循环结束跳出, 否则对这个孩子调用dispatchTransformedTouchEvent, 并在返回true时构造这个孩子的target并插入到链表头部, 然后结束循环, 以上内容都是仅在DOWN事件时发生. 这就意味着, 如果一个childDOWN事件时dispatchTransformedTouchEvent返回true了, 那么它在链表中就应该存在对应的target. 用newTouchTarget这个变量来暂时存储, 而且这个target应该在链表靠头部的位置. 如果newTouchTarget为空(意味着dispatchTransformedTouchEvent返回false)但target链表不为空(意味着本ViewGroup中已经有一组target等待接收事件), 则newTouchTarget存储链表尾节点. 循环结束后, 根据target链表继续处理事件, 如果target链表为空说明本ViewGroup的孩子节点没有曾经将事件处理完毕过(一旦有过, 就会被链表保存)或者本次事件是DOWN事件但被取消或拦截了, 那么调用View.dispatchTouchEvent继续处理事件, 而该方法默认是调用自身的onTouchEvent. 如果target链表不为空, 遍历target链表把事件传给对应的View进行处理(划重点!!事件如果被标记为取消或者拦截, 则不继续传递给孩子节点, 事件要么由自身onTouchEvent处理, 要么传递给已经在target列表中的节点处理; 事件不被取消也不被拦截的情况下, 如果孩子节点有一个返回true, 则以后的非DOWN事件也只交给target列表中的节点处理, 不再继续传递给其他孩子; target列表中的节点则是调用其dispatchTouchEvent方法继续传递和处理事件), 有任意一个处理完毕则都标记本ViewGroup已处理完毕该事件(但是不会中断遍历). 在该过程中如果有View标记为下次UP事件时取消, 则同时从链表中删除并回收该target.

小结一下, 借助target链表, ViewGroup记录了处理DOWN事件的View集合, 并在下次其他事件传入时仅传给这个链表中的View, 这就实现了对连续事件的处理. 而且对于ViewonTouchEvent默认实现来说, 四种事件均会返回true, 即View本身默认就是会拦截事件的.

事件冲突

仔细理解了上一节的内容之后, 就不难预见事件冲突这种情况发生的原因以及解决方案了.
总的来说, 事件冲突的原因就是意料之外的View或者ViewGroup把事件拦截了, 从而我们想用来处理事件的View或者ViewGroup无法接收到事件.

典型的, ScrollView中嵌套ListView, 冲突产生的原因是ScrollView在滑动拖拽的过程中会让onInterceptTouchEvent返回true, 直接导致ScrollView拦截事件, 不会被传递到深一层的ListView(其他View也一样). 解决方案也很简单, 就是重写ScrollViewonInterceptTouchEvent, 告诉ScrollView哪些情况该拦截, 哪些情况不该拦截. 如果不能修改这个拦截事件的View怎么办呢? 也好办, 从自己可控的ViewGroup出发, 调用requestDisallowInterceptTouchEvent要求该ViewGroup及其祖先节点不拦截事件, 再视情况恢复这个标记位.
可以看出, 了解了Android系统各个层次对事件的分发处理和返回的策略之后, 遇到任何事件冲突问题我们都能够很容易找到症结所在并给出解决方案.

对于嵌套滑动, View定义了一个接口dispatchNestedScroll用来传递嵌套滑动事件. 今后有空我将会继续研读这一块的设计.

文章目录
  1. 1. View与ViewGroup类结构
  2. 2. View与Window
  3. 3. View绘制与重绘过程
  4. 4. View与ViewGroup事件分发
    1. 4.1. 事件产生
    2. 4.2. 事件分发和处理
    3. 4.3. 事件冲突